{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7ddf2db4",
   "metadata": {},
   "source": [
    "# RunnableLambda\n",
    "\n",
    "- Author: [Kenny Jung](https://www.linkedin.com/in/kwang-yong-jung)\n",
    "- Peer Review: [Junseong Kim](https://www.linkedin.com/in/%EC%A4%80%EC%84%B1-%EA%B9%80-591b351b2/), [Haseom Shin](https://github.com/IHAGI-c)\n",
    "- Proofread : [Chaeyoon Kim](https://github.com/chaeyoonyunakim)\n",
    "- This is a part of [LangChain Open Tutorial](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial)\n",
    "\n",
    "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/13-LangChain-Expression-Language/03-RunnableLambda.ipynb)\n",
    "[![Open in GitHub](https://img.shields.io/badge/Open%20in%20GitHub-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/LangChain-OpenTutorial/LangChain-OpenTutorial/blob/main/13-LangChain-Expression-Language/03-RunnableLambda.ipynb)\n",
    "\n",
    "\n",
    "## Overview\n",
    "\n",
    "```RunnableLambda``` provides a way to **integrate custom functions** into your LangChain pipeline.\n",
    "\n",
    "This allows you to **define your own custom functions** and execute them using ```RunnableLambda``` as part of their workflow.\n",
    "\n",
    "For example, you can build your own logic and execute the functions, performing different tasks such as:\n",
    "- Data preprocessing,\n",
    "- Calculations,\n",
    "- Interactions with external APIs, and\n",
    "- Any other custom logic you need in your chain.\n",
    "\n",
    "\n",
    "### Table of Contents\n",
    "\n",
    "- [Overview](#overview)\n",
    "- [Environment Setup](#environment-setup)\n",
    "- [How to Execute Custom Functions](#how-to-execute-custom-functions)\n",
    "- [Using RunnableConfig as Parameters](#using-runnableconfig-as-parameters)\n",
    "\n",
    "### References\n",
    "\n",
    "- [LangChain Python API Reference > langchain: 0.3.29 > runnables > RunnableLambda](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.base.RunnableLambda.html)\n",
    "- [LangChain Python API Reference > langchain: 0.3.29 > runnables > RunnableConfig](https://python.langchain.com/api_reference/core/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig)\n",
    "- [LangChain Python API Reference > docs > concepts > runnables > Configurable Runnables](https://python.langchain.com/docs/concepts/runnables/#configurable-runnables)\n",
    "----"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f993cae",
   "metadata": {},
   "source": [
    "## Environment Setup\n",
    "\n",
    "Setting up your environment is the first step. See the [Environment Setup](https://wikidocs.net/257836) guide for more details.\n",
    "\n",
    "**[Note]**\n",
    "- The ```langchain-opentutorial``` is a package of easy-to-use environment setup guidance, useful functions and utilities for tutorials.\n",
    "- Check out the [```langchain-opentutorial```](https://github.com/LangChain-OpenTutorial/langchain-opentutorial-pypi) for more details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "7c1ee933",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%capture --no-stderr\n",
    "%pip install langchain-opentutorial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d0715aab",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install required packages\n",
    "from langchain_opentutorial import package\n",
    "\n",
    "package.install(\n",
    "    [\n",
    "        \"langsmith\",\n",
    "        \"langchain\",\n",
    "        \"langchain_core\",\n",
    "        \"langchain_openai\",\n",
    "    ],\n",
    "    verbose=False,\n",
    "    upgrade=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb16c354",
   "metadata": {},
   "source": [
    "Alternatively, you can set and load ```OPENAI_API_KEY``` from a ```.env``` file.\n",
    "\n",
    "**[Note]** This is only necessary if you haven't already set ```OPENAI_API_KEY``` in previous steps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "1d3ee479",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Environment variables have been set successfully.\n"
     ]
    }
   ],
   "source": [
    "# Set environment variables\n",
    "from langchain_opentutorial import set_env\n",
    "\n",
    "set_env(\n",
    "    {\n",
    "        \"OPENAI_API_KEY\": \"\",\n",
    "        \"LANGCHAIN_API_KEY\": \"\",\n",
    "        \"LANGCHAIN_TRACING_V2\": \"true\",\n",
    "        \"LANGCHAIN_ENDPOINT\": \"https://api.smith.langchain.com\",\n",
    "        \"LANGCHAIN_PROJECT\": \"ConversationBufferWindowMemory\",\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "0b767b3b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from dotenv import load_dotenv\n",
    "\n",
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a8ce9e9",
   "metadata": {},
   "source": [
    "## How to Execute Custom Functions\n",
    "\n",
    "**Important Note**\n",
    "\n",
    "A limitation of ```RunnableLambda``` is that **custom functions can only accept a single argument**. You can wrap custom functions with ```RunnableLambda``` to use them in your pipeline.\n",
    "\n",
    "If you have a function that requires multiple parameters, create a wrapper function:\n",
    "1. Accepts a single input (typically a dictionary).\n",
    "2. Unpacks this input into multiple arguments inside the wrapper.\n",
    "3. Passes these arguments to your original function.\n",
    "\n",
    "For example:\n",
    "\n",
    "```python\n",
    "# Won't work with RunnableLambda\n",
    "def original_function(arg1, arg2, arg3):\n",
    "    pass\n",
    "\n",
    "# Will work with RunnableLambda\n",
    "def wrapper_function(input_dict):\n",
    "    return original_function(\n",
    "            input_dict['arg1'],\n",
    "            input_dict['arg2'],\n",
    "            input_dict['arg3']\n",
    ")\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "44c99a82",
   "metadata": {},
   "outputs": [],
   "source": [
    "from operator import itemgetter\n",
    "\n",
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "from langchain_core.runnables import RunnableLambda\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "\n",
    "# Function for returning the length of the text\n",
    "def length_function(text):\n",
    "    return len(text)\n",
    "\n",
    "\n",
    "# Function for multiplying the length of two texts\n",
    "def _multiple_length_function(text1, text2):\n",
    "    return len(text1) * len(text2)\n",
    "\n",
    "\n",
    "# Wrapper function for connecting the function that receives 2 arguments\n",
    "def multiple_length_function(\n",
    "    _dict,\n",
    "):  # Function for multiplying the length of two texts\n",
    "    return _multiple_length_function(_dict[\"text1\"], _dict[\"text2\"])\n",
    "\n",
    "\n",
    "# Create a prompt template\n",
    "prompt = ChatPromptTemplate.from_template(\"what is {a} + {b}?\")\n",
    "# Initialize the ChatOpenAI model\n",
    "model = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
    "\n",
    "# Connect the prompt and model to create a chain\n",
    "chain1 = prompt | model\n",
    "\n",
    "# Chain configuration\n",
    "chain = (\n",
    "    {\n",
    "        \"a\": itemgetter(\"input_1\") | RunnableLambda(length_function),\n",
    "        \"b\": {\"text1\": itemgetter(\"input_1\"), \"text2\": itemgetter(\"input_2\")}\n",
    "        | RunnableLambda(multiple_length_function),\n",
    "    }\n",
    "    | prompt\n",
    "    | model\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "959d3031",
   "metadata": {},
   "source": [
    "Execute the chain and verify the result."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "60df37cc",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'3 + 9 equals 12.'"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Execute the chain with the given arguments.\n",
    "chain.invoke({\"input_1\": \"bar\", \"input_2\": \"gah\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c2e1d85",
   "metadata": {},
   "source": [
    "## Using RunnableConfig as Parameters\n",
    "\n",
    "```RunnableLambda``` can optionally accept a [RunnableConfig](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html#langchain_core.runnables.config.RunnableConfig) object.\n",
    "\n",
    "This allows you to pass various configuration options to nested executions, including:\n",
    "- Callbacks: For tracking and monitoring function execution.\n",
    "- Tags: For labeling and organizing different runs.\n",
    "- Other configuration: Additional settings to control function behavior.\n",
    "\n",
    "For example, you can use ```RunnableConfig``` to:\n",
    "- Track function performance.\n",
    "- Add logging capabilities.\n",
    "- Group related operations using tags.\n",
    "- Configure error handling and retry logic.\n",
    "- Set timeouts and other execution parameters.\n",
    "\n",
    "This makes ```RunnableLambda``` highly configurable for complex workflows with fine-grained control over execution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0e319229",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "import json\n",
    "\n",
    "\n",
    "def parse_or_fix(text: str, config: RunnableConfig):\n",
    "    # Create a prompt template for fixing the next text\n",
    "    fixing_chain = (\n",
    "        ChatPromptTemplate.from_template(\n",
    "            \"Fix the following text:\\n\\ntext\\n{input}\\n\\nError: {error}\"\n",
    "            \" Don't narrate, just respond with the fixed data.\"\n",
    "        )\n",
    "        | ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
    "        | StrOutputParser()\n",
    "    )\n",
    "    # Try up to 3 times\n",
    "    for _ in range(3):\n",
    "        try:\n",
    "            # Parse the text as JSON\n",
    "            return json.loads(text)\n",
    "        except Exception as e:\n",
    "            # If parsing fails, call the fixing chain to fix the text\n",
    "            text = fixing_chain.invoke({\"input\": text, \"error\": e}, config)\n",
    "            print(f\"config: {config}\")\n",
    "    # If parsing fails, return \"Failed to parse\"\n",
    "    return \"Failed to parse\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "c8f4269a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "config: {'tags': ['my-tag'], 'metadata': {}, 'callbacks': <langchain_core.callbacks.manager.CallbackManager object at 0x12d7f7250>, 'recursion_limit': 25, 'configurable': {}}\n",
      "\n",
      "\n",
      "Modified result:\n",
      "{'foo': 'bar'}\n"
     ]
    }
   ],
   "source": [
    "from langchain.callbacks import get_openai_callback\n",
    "\n",
    "with get_openai_callback() as cb:\n",
    "    # Call the parse_or_fix function using RunnableLambda\n",
    "    output = RunnableLambda(parse_or_fix).invoke(\n",
    "        input=\"{foo:: bar}\",\n",
    "        config={\"tags\": [\"my-tag\"], \"callbacks\": [cb]},  # Pass the config\n",
    "    )\n",
    "    # Print the modified result\n",
    "    print(f\"\\n\\nModified result:\\n{output}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "7da80691",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'foo': 'bar'}\n"
     ]
    }
   ],
   "source": [
    "# Check the output\n",
    "print(output)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "f960a244",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Tokens Used: 59\n",
      "\tPrompt Tokens: 52\n",
      "\t\tPrompt Tokens Cached: 0\n",
      "\tCompletion Tokens: 7\n",
      "\t\tReasoning Tokens: 0\n",
      "Successful Requests: 1\n",
      "Total Cost (USD): $1.1999999999999997e-05\n"
     ]
    }
   ],
   "source": [
    "# Check the callback\n",
    "print(cb)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "opentutorial",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
